/*
 * This file is open source software, licensed to you under the terms
 * of the Apache License, Version 2.0 (the "License").  See the NOTICE file
 * distributed with this work for additional information regarding copyright
 * ownership.  You may not use this file except in compliance with the License.
 *
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
/*
 * Copyright (C) 2017 ScyllaDB
 * Copyright (c) 2022 StoneAtom, Inc. All rights reserved.
 */

// The purpose of the hacks here is to workaround C++ exception scalability
// problem with gcc and glibc. For the best result gcc-7 is required.
//
// To summarize all the locks we have now and their purpose:
// 1. There is a lock in _Unwind_Find_FDE (libgcc) that protects
//    list of "Frame description entries" registered with __register_frame*
//    functions. The catch is that dynamically linked binary do not do that,
//    so all that it protects is checking that a certain list is empty.
//    This lock no longer relevant in gcc-7 since there is a patch there
//    that checks that the list is empty outside of the lock and it will be
//    always true for us.
// 2. The lock in dl_iterate_phdr (glibc) that protects loaded object
//    list against runtime object loading/unloading.
//
// To get rid of the first lock using gcc-7 is required.
//
// To get rid of the second one we can use the fact that we do not
// load/unload objects dynamically (at least for now). To do that we
// can mirror all elf header information in seastar and provide our
// own dl_iterate_phdr symbol which uses this mirror without locking.
//
// Unfortunately there is another gotcha in this approach: dl_iterate_phdr
// supplied by glibc never calls more then one callback simultaneously as an
// unintended consequences of the lock there, but unfortunately libgcc relies
// on that to maintain small cache of translations. The access to the cache is
// not protected by any lock since up until now only one callback could have
// run at a time. But luckily libgcc cannot use the cache if older version
// of dl_phdr_info is provided to the callback because the older version
// did not have an indication that loaded object list may have changed,
// so libgcc does not know when cache should be invalidated and disables it
// entirely. By calling the callback with old version of dl_phdr_info from
// our dl_iterate_phdr we can effectively make libgcc callback thread safe.

#ifndef NO_EXCEPTION_HACK

#include "exception_hacks.h"
#include <dlfcn.h>
#include <link.h>
#include <cassert>
#include <cstddef>
#include <vector>

namespace stonedb {
namespace base {

using dl_iterate_fn = int (*)(int (*callback)(struct dl_phdr_info *info, size_t size, void *data), void *data);

static dl_iterate_fn dl_iterate_phdr_org() {
  static dl_iterate_fn org = [] {
    auto org = (dl_iterate_fn)dlsym(RTLD_NEXT, "dl_iterate_phdr");
    assert(org);
    return org;
  }();
  return org;
}

static std::vector<dl_phdr_info> phdrs_cache;

void init_phdr_cache() {
  // Fill out elf header cache for access without locking.
  // This assumes no dynamic object loading/unloading after this point
  dl_iterate_phdr_org()(
      [](struct dl_phdr_info *info, [[maybe_unused]] size_t size, [[maybe_unused]] void *data) {
        phdrs_cache.push_back(*info);
        return 0;
      },
      nullptr);
}

}  // namespace base
}  // namespace stonedb

extern "C" [[gnu::visibility("default")]] [[gnu::externally_visible]] int dl_iterate_phdr(
    int (*callback)(struct dl_phdr_info *info, size_t size, void *data), void *data) {
  if (!stonedb::base::phdrs_cache.size()) {
    // Cache is not yet populated, pass through to original function
    return stonedb::base::dl_iterate_phdr_org()(callback, data);
  }
  int r = 0;
  for (auto h : stonedb::base::phdrs_cache) {
    // Pass dl_phdr_info size that does not include dlpi_adds and dlpi_subs.
    // This forces libgcc to disable caching which is not thread safe and
    // requires dl_iterate_phdr to serialize calls to callback. Since we do
    // not serialize here we have to disable caching.
    r = callback(&h, offsetof(dl_phdr_info, dlpi_adds), data);
    if (r) {
      break;
    }
  }
  return r;
}

#endif  // NO_EXCEPTION_HACK
